package com.ck.server.controller.test.login;

import cn.hutool.core.codec.Base64;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ck.common.bean.*;
import com.ck.common.constant.AjaxResult;
import com.ck.common.constant.Constants;
import com.ck.common.utils.jwt.RedisCache;
import com.ck.common.utils.random.CtUtilRandom;
import com.ck.common.utils.security.RsaUtil;
import com.ck.common.utils.security.sign.CtMD5;
import com.ck.common.vo.RSALoginVo;
import com.ck.common.vo.LoginUserVo;
import com.ck.framework.web.service.TokenService;
import com.ck.framework.security.threadlocal.AuthenticationContextHolder;
import com.ck.system.service.AuthService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author ck
 * @date 2024/1/19 14:10
 * desc: RSA+AES实现前后端接口加解密
 */
@Slf4j
@RestController
@RequestMapping("/rsa")
@Api(tags = {"RSA+AES 登录接口"})
public class RsaAndAesLoginController {

    @Autowired
    RedisCache redisCache;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    TokenService tokenService;

    @Autowired
    AuthService authService;

    /**
     * 使用RSA算法生成公钥和私钥，以公钥作为KeyId,经过MD5编码之后作为缓存的键名,把公、私钥缓存至redis中
     * @return
     * @throws Exception
     */
    @ApiOperation("获取公钥")
    @GetMapping("/getPubKey")
    public AjaxResult getPubKey() throws Exception {
        //生成密钥对
        Map keyMap = RsaUtil.createKeys(1024);
        String publicKey = (String) keyMap.get("publicKey");
        String privateKey = (String) keyMap.get("privateKey");

        McApiRsa mcApiRsa = new McApiRsa();
        mcApiRsa.setPrivateKey(privateKey);
        mcApiRsa.setPublicKey(publicKey);

        //以公钥字符为数据，生成MD5的KeyId
        String keyId = CtMD5.MD5_32(publicKey);
        McApiPubKey mcApiPubKey = new McApiPubKey();
        mcApiPubKey.setKeyId(keyId);
        mcApiPubKey.setPubKey(publicKey);

        //以keyId作为键名，将公钥和密钥存入缓存
        String cacheKey = Constants.RedisKey.PUBLIC_KEY+keyId;
        redisCache.setCacheObject(cacheKey, mcApiRsa, 1, TimeUnit.DAYS);
        log.info("PubKey redis key: " + cacheKey + "; val: " + mcApiRsa.toString());
        //将KeyId、RSA公钥返回给前端
        return AjaxResult.success(mcApiPubKey);
    }


    /**
     * 在登录之前，前端会对登录报文进行加密(公钥加密)
     * 在到达controller之前会先经过切面，在切面里通过传过来的keyId去redis中拿到RSA私钥对请求报文进行解密
     * 登录成功后，在切面里会对返回内容进行AES加密，再返回给前端
     * @param rsaLoginVo
     * @return
     */
    @ApiOperation(value = "登录")
    @PostMapping("/login")
    public McApiResp login(@ApiParam(name = "req")@RequestBody RSALoginVo rsaLoginVo) {
        //通过keyId查询RSA公钥、私钥信息
        String keyId = rsaLoginVo.getKeyId();
        JSONObject jsonObject = JSON.parseObject(rsaLoginVo.getD());

        String userName = jsonObject.getString("loginName");
        String passWord = jsonObject.getString("pwd");

        if (StringUtils.isEmpty(keyId) || StringUtils.isEmpty(userName) || StringUtils.isEmpty(passWord)) {
            return McApiResp.fail("参数错误");
        }
        log.info("login in authorize: " + userName + ", pwd: " + passWord);

        //身份验证
        Authentication authentication = null;
        try {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, passWord);

            //用户名、密码存入threadLocal中
            AuthenticationContextHolder.setContext(authenticationToken);
            //该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
        } catch (BadCredentialsException e) {
            return McApiResp.fail("用户名或密码错误");
        }
        finally {
            //清除ThreadLocal，防止内存溢出
            AuthenticationContextHolder.clearContext();
        }
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //获得用户信息，即UserDetails对象
        Object principal = authentication.getPrincipal();
        Long id = null;
        String token=null;
        if (principal instanceof LoginUserVo){
            LoginUserVo loginUser = (LoginUserVo) principal;
            id = loginUser.getUser().getUserId();
            token = tokenService.createToken(loginUser);
        }

        /**生成业务接口AES加解密算法的秘钥和偏移量*/
        //偏移量
        String vipara = CtMD5.MD5_16(userName);
        //秘钥
        String aesKey = CtMD5.MD5_16(passWord);
        log.info("vipara: " + vipara + ", aesKey: " + aesKey);

        /**生成keyToken*/
        //获取加密key
        String keyToken = CtMD5.MD5_32(keyId);
        //keyToken放入缓存
        McAesInfo aes = new McAesInfo();

        McAesInfo mcAesInfo = this.genAesInfo();
        //返回给前端秘钥
        aes.setAesKey(Base64.encode(mcAesInfo.getAesKey()));
        //返回给前端的偏移量
        aes.setVipara(Base64.encode(mcAesInfo.getVipara()));
        aes.setKeyToken(keyToken);

        McApiUserAuth mcApiUserAuth = new McApiUserAuth();
        mcApiUserAuth.setToken(keyToken);
        mcApiUserAuth.setAesInfo(mcAesInfo);
        /**以keyToken为键名,将AES加密秘钥、偏移量存入缓存*/
        authService.setUserAuthMapInRedisCache(keyToken, mcApiUserAuth ,id);

        /**将jwt用户令牌toekn、业务接口AES信息、kenToken封装为JSON对象*/
        AuthResp resp = new AuthResp();
        resp.setToken(token);
        resp.setAesInfo(aes);
        resp.setKeyToken(keyToken);
        resp.setVipara(vipara);
        resp.setAesKey(aesKey);

        Map<String, Object> retMap = new HashMap<>();
        retMap.put("data", resp);

        return McApiResp.success(retMap);
    }

    private McAesInfo genAesInfo() {
        McAesInfo mcAesInfo = new McAesInfo();
        String formatStr = CtUtilRandom.CHARS_LOW.toUpperCase() + CtUtilRandom.CHARS_LOW + CtUtilRandom.CHARS_NUM;
        String vipara = CtMD5.MD5_16(CtUtilRandom.getRandomStr(formatStr, 32));
        mcAesInfo.setAesKey(CtMD5.MD5_16(CtUtilRandom.getRandomStr(formatStr, 32)));
        mcAesInfo.setVipara(vipara);
        return mcAesInfo;
    }
}
